using System;
using System.IO;
using System.Xml;

namespace CodeBackpack.Plist
{
    public class PropertyListXmlReader : PropertyListReader
    {
        private static readonly XmlResolver dtdResolver = new EmbeddedDtdResolver();
        private readonly XmlReader xmlReader;

        /// <summary>
        /// Gets the internal <see cref="XmlResolver"/> that uses emebedded copy of the official
        /// plist V1.0 DTD.
        /// </summary>
        public static XmlResolver DtdResolver
        {
            get { return dtdResolver; }
        }

        public PropertyListXmlReader(TextReader textReader)
        {
            if (textReader == null)
            {
                throw new ArgumentNullException("textReader");
            }
            XmlReaderSettings settings = new XmlReaderSettings();
            settings.ProhibitDtd = false;
            settings.ValidationType = ValidationType.DTD;
            settings.XmlResolver = dtdResolver;
            xmlReader = XmlReader.Create(textReader, settings);
        }

        public override void Dispose()
        {
            (xmlReader as IDisposable).Dispose();
        }

        public override void ReadProlog()
        {
            xmlReader.MoveToContent();
            AssertElement(XmlTags.Plist);
            AssertAttribute(XmlTags.VersionAttr, "1.0");
            if (!xmlReader.IsEmptyElement)
            {
                xmlReader.Read();
            }
        }

        public override bool IsEmpty()
        {
            return (xmlReader.IsStartElement(XmlTags.Plist) && xmlReader.IsEmptyElement) ||
                   (xmlReader.NodeType == XmlNodeType.EndElement && xmlReader.LocalName == XmlTags.Plist);
        }

        public override void ReadEpilog()
        {
            if (!AtContainerEnd(XmlTags.Plist))
            {
                throw new XmlException("Expected end tag for 'plist' was not encountered!");
            }
        }

        public override bool AtListStart()
        {
            return AtContainerStart(XmlTags.Array);
        }

        public override bool AtDictStart()
        {
            return AtContainerStart(XmlTags.Dict);
        }

        public override bool AtContainerEnd()
        {
            return AtContainerEnd(XmlTags.Array) || AtContainerEnd(XmlTags.Dict);
        }

        public override String ReadDictKey()
        {
            if (xmlReader.IsStartElement(XmlTags.Key))
            {
                string key = xmlReader.ReadElementContentAsString();
                if (key == "")
                {
                    throw new XmlException("Element 'key' has no content!");
                }
                return key;
            }
            else
            {
                throw new XmlException("Element 'key' was expected but not encountered!");
            }
        }

        public override Object ReadScalar()
        {
            if (xmlReader.IsStartElement(XmlTags.Data))
            {
                string base64text = xmlReader.ReadElementContentAsString();
                return Convert.FromBase64String(base64text);
            }
            else if (xmlReader.IsStartElement(XmlTags.String))
            {
                return xmlReader.ReadElementContentAsString();
            }
            else if (xmlReader.IsStartElement(XmlTags.Integer))
            {
                long longInt = xmlReader.ReadElementContentAsLong();
                if (longInt < int.MinValue || longInt > int.MaxValue)
                {
                    return longInt;
                }
                else
                {
                    return (int) longInt;
                }
            }
            else if (xmlReader.IsStartElement(XmlTags.Real))
            {
                return xmlReader.ReadElementContentAsDouble();
            }
            else if (xmlReader.IsStartElement(XmlTags.True))
            {
                AssertEmptyOrNoContent(XmlTags.True);
                return true;
            }
            else if (xmlReader.IsStartElement(XmlTags.False))
            {
                AssertEmptyOrNoContent(XmlTags.False);
                return false;
            }
            else if (xmlReader.IsStartElement(XmlTags.Date))
            {
                return xmlReader.ReadElementContentAsDateTime();
            }
            else if (xmlReader.IsStartElement())
            {
                throw new XmlException(String.Format("Element '{0}' is not supported!", xmlReader.LocalName));
            }
            else
            {
                throw new XmlException(String.Format("Unexpected XML node '{0}'!", xmlReader.NodeType));
            }
        }

        private void AssertEmptyOrNoContent(String name)
        {
            if (xmlReader.IsEmptyElement)
            {
                xmlReader.Read();
            }
            else
            {
                xmlReader.Read();
                xmlReader.MoveToContent();
                if (xmlReader.NodeType != XmlNodeType.EndElement)
                {
                    throw new XmlException(String.Format("Element '{0}' must not have any content!", name));
                }
                xmlReader.Read();
            }
        }

        private bool AtContainerStart(String container)
        {
            bool result = xmlReader.IsStartElement(container);
            if (result)
            {
                if (!xmlReader.IsEmptyElement)
                {
                    xmlReader.Read();
                    xmlReader.MoveToContent();
                }
            }
            return result;
        }

        private bool AtContainerEnd(string container)
        {
            if ((xmlReader.IsStartElement(container) && xmlReader.IsEmptyElement) ||
                (xmlReader.NodeType == XmlNodeType.EndElement && xmlReader.LocalName == container))
            {
                xmlReader.Read();
                return true;
            }
            else
            {
                return false;
            }
        }

        private void AssertElement(String name)
        {
            if (xmlReader.NodeType != XmlNodeType.Element || xmlReader.LocalName != name)
            {
                throw new XmlException(String.Format("Current XML node is not '{0}' element!", name));
            }
        }

        private void AssertAttribute(String name, String expectedValue)
        {
            if (xmlReader.GetAttribute(name) != expectedValue)
            {
                throw new XmlException(String.Format("Attribute '{0}' is not equal to '{1}'!", name, expectedValue));
            }
        }
    }
}